package com.yiyi.credit.manage.abutment.common.utils;

import com.yiyi.credit.manage.abutment.common.anno.Converter;
import com.yiyi.credit.manage.abutment.common.anno.ExcelColumn;
import com.yiyi.credit.manage.abutment.common.converter.FieldConverter;
import ognl.Ognl;
import ognl.OgnlException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;


public class ExcelUtil {
    private static final Logger logger = LoggerFactory.getLogger(ExcelUtil.class);

    private static final Map<Class, Map<String, AnnoMeta>> ATTR_MAP = new ConcurrentHashMap<>();
    private static final Map<Class, List<String>> TITLE_MAP = new ConcurrentHashMap<>();
    private static final Map<Class, FieldConverter> converterCache = new ConcurrentHashMap<>();
    private static final Map<String, FieldConverter> converters = new ConcurrentHashMap<>();

    public static <T> boolean exportExcel(Class<T> clazz, List<T> objects, String filePath)
            throws OgnlException, IOException {

        Map<String, AnnoMeta> fieldMap = getFields(clazz);
        List<String> titles = TITLE_MAP.get(clazz);

        HSSFWorkbook workbook = new HSSFWorkbook();
        HSSFCellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(XSSFCellStyle.ALIGN_CENTER);
        cellStyle.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER);
        try (OutputStream is = new FileOutputStream(filePath)) {
            HSSFSheet sheet = workbook.createSheet();
            HSSFRow titleRow = sheet.createRow(0);
            for (int i = 0; i < titles.size(); i++) {
                HSSFCell cell = titleRow.createCell(i);
                cell.setCellType(Cell.CELL_TYPE_STRING);
                cell.setCellValue(titles.get(i));
            }

            for (int i = 0; i < objects.size(); i++) {
                HSSFRow valueRow = sheet.createRow(i + 1);
                T obj = objects.get(i);
                for (int j = 0; j < titles.size(); j++) {
                    String title = titles.get(j);
                    String fieldName = fieldMap.get(title).getFieldName();
                    Object value = Ognl.getValue(fieldName, obj);
                    HSSFCell cell = valueRow.createCell(j, Cell.CELL_TYPE_STRING);
                    cell.setCellStyle(cellStyle);
                    cell.setCellValue(converter(clazz, fieldName, value));
                }
            }

            int lastRow = sheet.getLastRowNum();
            Map<String, BigDecimal> summery = new HashMap<>();
            Map<String, List<int[]>> mergeRegions = new HashMap<>();
            for (int i = 0; i < lastRow; i++) {
                HSSFRow row = sheet.getRow(i);
                for (int j = 0; j < row.getLastCellNum(); j++) {
                    String title = titles.get(j);
                    if (!needMerge(clazz, title)) continue;

                    HSSFCell cell = row.getCell(j);
                    String val = cell.getStringCellValue();
                    int d = i;
                    while (d <= lastRow) {
                        HSSFRow nextRow = sheet.getRow(d);
                        HSSFCell nextCell = nextRow.getCell(j);
                        if (StringUtils.equals(val, nextCell.getStringCellValue()))
                            d++;
                        else
                            break;
                    }

                    if (d - 1 > i) {
                        CellRangeAddress rangeAddress = new CellRangeAddress(i, d - 1, j, j);
                        sheet.addMergedRegion(rangeAddress);
                        List<int[]> ranges = mergeRegions.computeIfAbsent(title, k -> new LinkedList<>());
                        ranges.add(new int[]{i, d - 1});
                    }
                }
            }

            for (int i = 0; i <= lastRow; i++) {
                HSSFRow row = sheet.getRow(i);
                for (int j = 0; j < row.getLastCellNum(); j++) {
                    String title = titles.get(j);
                    if (!needSummery(clazz, title)) {
                        continue;
                    }

                    boolean added = false;
                    List<int[]> list = mergeRegions.get(title);
                    if (list != null) {
                        for (int[] a : list) {
                            if (a[0] < i && a[1] >= i) {
                                added = true;
                                break;
                            }
                        }
                    }

                    if (added)
                        continue;

                    HSSFCell cell = row.getCell(j);
                    BigDecimal bd = summery.get(title);
                    if (bd == null)
                        bd = BigDecimal.ZERO;

                    summery.put(title, BigDecimalUtil.add(BigDecimalUtil.valueOf(cell.getStringCellValue()), bd));
                }
            }

            if (!CollectionUtils.isEmpty(summery)) {
                HSSFRow summeryRow = sheet.createRow(sheet.getLastRowNum() + 1);
                HSSFRow summeryValueRow = sheet.createRow(sheet.getLastRowNum() + 1);
                summery.forEach((k, v) -> {
                    int index = titles.indexOf(k);
                    HSSFCell cell = summeryRow.createCell(index);
                    cell.setCellStyle(cellStyle);
                    cell.setCellValue("合计");

                    HSSFCell valCell = summeryValueRow.createCell(index);
                    valCell.setCellStyle(cellStyle);
                    valCell.setCellValue(String.valueOf(v));
                });
            }
            workbook.write(is);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw e;
        }

        return true;
    }

    public static boolean exportExcel(String[][] values, String filePath) throws IOException {
        HSSFWorkbook workbook = new HSSFWorkbook();
        try (OutputStream is = new FileOutputStream(filePath)) {
            HSSFSheet sheet = workbook.createSheet();
            for (int i = 0; i < values.length; i++) {
                HSSFRow valueRow = sheet.createRow(i);
                for (int j = 0; j < values[i].length; i++) {
                    HSSFCell cell = valueRow.createCell(j, Cell.CELL_TYPE_STRING);
                    cell.setCellValue(values[i][j]);
                }
            }

            workbook.write(is);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw e;
        }

        return true;
    }

    private static boolean needMerge(Class clazz, String title) {
        AnnoMeta meta = getMeta(clazz, title);
        return meta != null && meta.getColumn().mergeSame();
    }

    private static boolean needSummery(Class clazz, String title) {
        AnnoMeta meta = getMeta(clazz, title);
        return meta != null && meta.getColumn().summery();
    }

    private static AnnoMeta getMeta(Class clazz, String title) {
        Map<String, AnnoMeta> map = ATTR_MAP.get(clazz);
        if (map != null) {
            return map.get(title);
        }

        return null;
    }

    private static String converter(Class clazz, String fieldName, Object value) {
        if (value == null)
            return "";
        FieldConverter converter = converters.get(composedKey(clazz, fieldName));
        return converter == null ? String.valueOf(value) : converter.convert(value);
    }

    private static String composedKey(Class clazz, String fieldName) {
        return clazz.getName().concat(".").concat(fieldName);
    }

    private static Map<String, AnnoMeta> getFields(Class clazz) {
        Map<String, AnnoMeta> map = ATTR_MAP.get(clazz);
        logger.info("loading class meta info : {}", clazz.getName());

        if (map == null) {
            map = new HashMap<>();
            ATTR_MAP.put(clazz, map);
            Map<Integer, String> titleMap = new TreeMap<>();
            ReflectionUtils.doWithFields(clazz, field -> {
                ExcelColumn column = AnnotationUtils.findAnnotation(field, ExcelColumn.class);
                if (column == null) {
                    logger.info("No @ExcelColumn found for field : {}", field.getName());
                    return;
                }

                int order = column.order();
                String title = column.title();
                logger.info("found @ExcelColumn for field : {}, title: {}, order: {}",
                        field.getName(), title, order);

                titleMap.put(order, title);
                ATTR_MAP.get(clazz).put(title, new AnnoMeta(column, field.getName()));

                Converter converter = AnnotationUtils.findAnnotation(field, Converter.class);
                if (converter != null) {
                    Class cls = converter.value();
                    try {
                        FieldConverter instance = createConverter(cls);
                        converters.put(composedKey(clazz, field.getName()), instance);
                    } catch (InstantiationException e) {
                        logger.error("Error create field converter for field : {}", field.getName());
                    }
                }
            });

            TITLE_MAP.put(clazz, new ArrayList<>(titleMap.values()));
        }

        return map;
    }

    private static FieldConverter createConverter(Class converterClass) throws IllegalAccessException, InstantiationException {
        FieldConverter converter = converterCache.get(converterClass);
        if (converter == null) {
            converter = (FieldConverter) converterClass.newInstance();
            converterCache.put(converterClass, converter);
        }

        return converter;
    }

    public static <T> void sendExcel(Class<T> clazz, List<T> objects, String fileName, HttpServletResponse response) throws IOException, OgnlException {
        String dir = GlobalConfig.lookupKey("user.home", "");
        if (!StringUtils.isEmpty(dir)) {
            if (!dir.endsWith("/"))
                dir = dir + "/";
        }
        boolean flag = ExcelUtil.exportExcel(clazz, objects, dir + fileName);
        if (flag) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/vnd.ms-excel; charset=utf-8");
            response.addHeader("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("GB2312"), "ISO8859-1"));
            try (InputStream is = new FileInputStream(dir + fileName);
                 OutputStream os = response.getOutputStream()) {
                IOUtils.copy(is, os);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                throw e;
            }

            return;
        }

        throw new RuntimeException("导出失败!");
    }


    public static void commonExport(HttpServletResponse response, File file, boolean flag) throws IOException {
        if (flag) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/vnd.ms-excel; charset=utf-8");
            response.addHeader("Content-Disposition", "attachment; filename=" + new String(file.getName().getBytes("GB2312"), "ISO8859-1"));
            try (InputStream is = new FileInputStream(file); OutputStream os = response.getOutputStream()) {
                IOUtils.copy(is, os);
            }
        }
    }


    public static String getFilePath(String fileName) {
        String dir = GlobalConfig.lookupKey("user.home", "");
        if (!StringUtils.isEmpty(dir)) {
            if (!dir.endsWith("/"))
                dir = dir + "/";
        }
        String name = DateUtil.dateToString("yyyy年MM月dd日", new Date()) + fileName + ".xls";
        return dir + name;
    }

    private static class AnnoMeta {
        private ExcelColumn column;
        private String fieldName;

        public AnnoMeta(ExcelColumn column, String fieldName) {
            this.column = column;
            this.fieldName = fieldName;
        }

        public ExcelColumn getColumn() {
            return column;
        }

        public void setColumn(ExcelColumn column) {
            this.column = column;
        }

        public String getFieldName() {
            return fieldName;
        }

        public void setFieldName(String fieldName) {
            this.fieldName = fieldName;
        }
    }
}
